package com.nth.android.ar;

import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.hardware.GeomagneticField;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.PowerManager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;

public class ARAugmentedView extends View implements LocationListener, SensorEventListener, OnTouchListener {

	private PowerManager.WakeLock wakeLock;
	private LocationManager locationManager;
	private SensorManager sensorManager;
	private Sensor sensorGrav, sensorMag;

	private Bitmap radarBackground;
	private Bitmap radarBackgroundBitmap;
	private int radarBackgroundResourceId;
	private Bitmap radarPoi;
	private Bitmap radarPoiBitmap;
	private int radarPoiResourceId;
	private boolean showRadar;

	private int histRotationIndex = 0;
	private Matrix3 histRotation[] = new Matrix3[64];
	private Matrix3 m1 = new Matrix3();
	private Matrix3 m2 = new Matrix3();
	private Matrix3 m3 = new Matrix3();
	private Matrix3 m4 = new Matrix3();
	private Matrix3 tempRotation = new Matrix3();
	private Matrix3 finalRotation = new Matrix3();
	private Matrix3 smoothRotation = new Matrix3();
	private Matrix3 rotationMatrix = new Matrix3();
	private float rotationDataTemp[] = new float[9];
	private float rotationData[] = new float[9];
	private float inclinationData[] = new float[9];
	private float gravityData[] = new float[3];
	private float magneticData[] = new float[3];

	private ARCamera camera;
	private Location location;

	private float addX = 0, addY = 0;

	@SuppressWarnings("unused")
	private float pitch;
	private int bearing;
	private int orientation;

	private Paint paint;
	private Paint linePaint;

	private int radarRadius;
	private int radarPadding;
	private Paint radiusPaint;
	private final int RADAR_RADIUS_DEFAULT = 40;
	private final int RADAR_PADDING_DEFAULT = 10;
	@SuppressWarnings("unused")
	private String radarDirectionString;

	private List<AROverlay> overlays;

	public ARAugmentedView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		init();
	}

	public ARAugmentedView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init();
	}

	public ARAugmentedView(Context context) {
		super(context);
		init();
	}

	public Bitmap getRadarBackgroundBitmap() {
		return radarBackgroundBitmap;
	}

	public void setRadarBackgroundBitmap(Bitmap radarBackgroundBitmap) {
		this.radarBackgroundBitmap = radarBackgroundBitmap;
		radarBackground = radarBackgroundBitmap;
		radarRadius = radarBackground.getWidth() / 2;
	}

	public int getRadarBackgroundResourceId() {
		return radarBackgroundResourceId;
	}

	public void setRadarBackgroundResourceId(int radarBackgroundResourceId) {
		this.radarBackgroundResourceId = radarBackgroundResourceId;
		radarBackground = BitmapFactory.decodeResource(getContext().getResources(), radarBackgroundResourceId);
		radarRadius = radarBackground.getWidth() / 2;
	}

	public Bitmap getRadarPoiBitmap() {
		return radarPoiBitmap;
	}

	public void setRadarPoiBitmap(Bitmap radarPoiBitmap) {
		this.radarPoiBitmap = radarPoiBitmap;
		radarPoi = radarPoiBitmap;
	}

	public int getRadarPoiResourceId() {
		return radarPoiResourceId;
	}

	public void setRadarPoiResourceId(int radarPoiResourceId) {
		this.radarPoiResourceId = radarPoiResourceId;
		radarPoi = BitmapFactory.decodeResource(getContext().getResources(), radarPoiResourceId);
	}

	public boolean isShowRadar() {
		return showRadar;
	}

	public void setShowRadar(boolean showRadar) {
		this.showRadar = showRadar;
	}

	private void init() {
		overlays = new ArrayList<AROverlay>();

		radarRadius = RADAR_RADIUS_DEFAULT;
		radarPadding = RADAR_PADDING_DEFAULT;

		paint = new Paint();
		paint.setColor(Color.RED);
		paint.setTextSize(16);
		paint.setTypeface(Typeface.DEFAULT_BOLD);
		paint.setStyle(Paint.Style.FILL);
		paint.setAntiAlias(true);

		linePaint = new Paint();
		linePaint.setStrokeWidth(2);
		linePaint.setColor(Color.WHITE);
		linePaint.setAlpha(128);
		linePaint.setStyle(Paint.Style.STROKE);
		linePaint.setAntiAlias(true);

		radiusPaint = new Paint();
		radiusPaint.setColor(Color.BLACK);
		radiusPaint.setAlpha(128);
		radiusPaint.setStyle(Paint.Style.FILL);
		radiusPaint.setAntiAlias(true);

		PowerManager powerManager = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
		if (powerManager != null) {
			wakeLock = powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, getClass().getName());
		}

		sensorManager = (SensorManager) getContext().getSystemService(Context.SENSOR_SERVICE);
		if (sensorManager != null) {
			List<Sensor> sensors = sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER);
			if (sensors.size() > 0) {
				sensorGrav = sensors.get(0);
			}
			sensors = sensorManager.getSensorList(Sensor.TYPE_MAGNETIC_FIELD);
			if (sensors.size() > 0) {
				sensorMag = sensors.get(0);
			}
		}

		locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE);

		setOnTouchListener(this);
	}

	public void start() {
		wakeLock.acquire();

		sensorManager.registerListener(this, sensorGrav, SensorManager.SENSOR_DELAY_GAME);
		sensorManager.registerListener(this, sensorMag, SensorManager.SENSOR_DELAY_GAME);

		orientation = getContext().getResources().getConfiguration().orientation;

		double angleX = Math.toRadians(-90);
		double angleY = Math.toRadians(-90);
		m1.set(1f, 0f, 0f, 0f, (float) Math.cos(angleX), (float) -Math.sin(angleX), 0f, (float) Math.sin(angleX), (float) Math.cos(angleX));
		m2.set(1f, 0f, 0f, 0f, (float) Math.cos(angleX), (float) -Math.sin(angleX), 0f, (float) Math.sin(angleX), (float) Math.cos(angleX));
		m3.set((float) Math.cos(angleY), 0f, (float) Math.sin(angleY), 0f, 1f, 0f, (float) -Math.sin(angleY), 0f, (float) Math.cos(angleY));
		m4.toIdentity();

		for (int i = 0; i < histRotation.length; i++) {
			histRotation[i] = new Matrix3();
		}

		try {
			Criteria criteria = new Criteria();
			criteria.setAccuracy(Criteria.ACCURACY_FINE);

			if (locationManager != null) {
				String bestProvider = locationManager.getBestProvider(criteria, true);
				locationManager.requestLocationUpdates(bestProvider, 10000, 10, this);

				Location lastLocation = locationManager.getLastKnownLocation(bestProvider);
				onLocationChanged(lastLocation);

				GeomagneticField gmf = new GeomagneticField((float) lastLocation.getLatitude(), (float) lastLocation.getLongitude(), (float) lastLocation.getAltitude(), System
						.currentTimeMillis());

				angleY = Math.toRadians(-gmf.getDeclination());
				m4.set((float) Math.cos(angleY), 0f, (float) Math.sin(angleY), 0f, 1f, 0f, (float) -Math.sin(angleY), 0f, (float) Math.cos(angleY));
				// set declination
			}
		} catch (Exception ex) {
			Log.e(getClass().getName(), "AR initialize error");
		}
	}

	public void stop() {
		wakeLock.release();

		try {
			sensorManager.unregisterListener(this, sensorGrav);
		} catch (Exception ex) {
		}
		try {
			sensorManager.unregisterListener(this, sensorMag);
		} catch (Exception ex) {
		}

		try {
			locationManager.removeUpdates(this);
		} catch (Exception ex) {
		}
	}

	public void addOverlay(AROverlay overlay) {
		overlays.add(overlay);
		refreshItemLocations();
	}

	public void addOverlays(List<AROverlay> overlays) {
		this.overlays.addAll(overlays);
		refreshItemLocations();
	}

	public void removeOverlay(AROverlay overlay) {
		overlays.remove(overlay);
	}

	public void removeAllOverlays() {
		overlays.clear();
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		int measuredWidth = 0;
		int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
		int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
		if (widthSpecMode == MeasureSpec.EXACTLY) {
			measuredWidth = widthSpecSize;
		} else {
			measuredWidth = getResources().getDisplayMetrics().widthPixels;
			if (widthSpecMode == MeasureSpec.AT_MOST) {
				measuredWidth = Math.min(measuredWidth, widthSpecSize);
			}
		}

		int measuredHeight = 0;
		int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
		int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
		if (heightSpecMode == MeasureSpec.EXACTLY) {
			measuredHeight = heightSpecSize;
		} else {
			measuredHeight = getResources().getDisplayMetrics().heightPixels;
			if (heightSpecMode == MeasureSpec.AT_MOST) {
				measuredHeight = Math.min(measuredHeight, heightSpecSize);
			}
		}

		setMeasuredDimension(measuredWidth, measuredHeight);
	}

	@Override
	protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
		super.onLayout(changed, left, top, right, bottom);
		camera = new ARCamera(right - left, bottom - top);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		try {
			camera.transform.set(rotationMatrix);
			calcPitchBearing(camera.transform);

			for (int i = 0; i < overlays.size(); i++) {
				overlays.get(i).calcAndDraw(canvas, camera, addX, addY, bearing);
			}
		} catch (Exception ex) {
		}

		// draw radar
		if (isShowRadar()) {
			canvas.save();
			canvas.translate(radarPadding + radarRadius, radarPadding + radarRadius);
			if (radarBackground == null) {
				canvas.drawCircle(0, 0, radarRadius, radiusPaint);
				canvas.rotate(-45 / 2);
				canvas.drawLine(0, 0, 0, -radarRadius, linePaint);
				canvas.rotate(45);
				canvas.drawLine(0, 0, 0, -radarRadius, linePaint);
				canvas.rotate(-45 / 2);
			} else {
				canvas.drawBitmap(radarBackground, -radarBackground.getWidth() / 2, -radarBackground.getHeight() / 2, null);
			}

			canvas.rotate(-bearing);
			double maxDistance = 0;
			for (int i = 0; i < overlays.size(); i++) {
				if (overlays.get(i) instanceof ARItemizedOverlay) {
					ARItemizedOverlay itemizedOverlay = (ARItemizedOverlay) overlays.get(i);
					if (maxDistance < itemizedOverlay.getMaxDistance()) {
						maxDistance = itemizedOverlay.getMaxDistance();
					}
				}
			}
			double scale = (maxDistance / radarRadius) * 1.2;
			float x, y;
			for (int i = 0; i < overlays.size(); i++) {
				if (overlays.get(i) instanceof ARItemizedOverlay) {
					ARItemizedOverlay itemizedOverlay = (ARItemizedOverlay) overlays.get(i);
					for (int j = 0; j < itemizedOverlay.size(); j++) {
						AROverlayItem arItem = itemizedOverlay.getItem(j);
						x = (float) (arItem.getLocationVector().x / scale);
						y = (float) (arItem.getLocationVector().z / scale);
						if (x * x + y * y < radarRadius * radarRadius) {
							if (radarPoi == null) {
								canvas.drawCircle(x, y, 3, paint);
							} else {
								canvas.drawBitmap(radarPoi, x - radarPoi.getWidth() / 2, y - radarPoi.getHeight() / 2, paint);
							}
						}
					}
				}
			}
			canvas.restore();

			// int range = (int) (bearing / (360.0 / 16.0));
			// switch (range) {
			// case 0:
			// case 15:
			// radarDirectionString = "N";
			// break;
			// case 1:
			// case 2:
			// radarDirectionString = "NE";
			// break;
			// case 3:
			// case 4:
			// radarDirectionString = "E";
			// break;
			// case 5:
			// case 6:
			// radarDirectionString = "SE";
			// break;
			// case 7:
			// case 8:
			// radarDirectionString = "S";
			// break;
			// case 9:
			// case 10:
			// radarDirectionString = "SW";
			// break;
			// case 11:
			// case 12:
			// radarDirectionString = "W";
			// break;
			// case 13:
			// case 14:
			// radarDirectionString = "NW";
			// break;
			// default:
			// radarDirectionString = "0";
			// break;
			// }
			// canvas.drawText(radarDirectionString, 10, 20, paint);
		}
	}

	@Override
	public void onAccuracyChanged(Sensor sensor, int accuracy) {
		if (sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
			if (accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
				// TODO dispatch unreliable
			}
		}
	}

	@Override
	public void onSensorChanged(SensorEvent event) {
		try {
			if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
				gravityData[0] = event.values[0];
				gravityData[1] = event.values[1];
				gravityData[2] = event.values[2];
			} else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
				magneticData[0] = event.values[0];
				magneticData[1] = event.values[1];
				magneticData[2] = event.values[2];
			}
			invalidate();

			SensorManager.getRotationMatrix(rotationDataTemp, inclinationData, gravityData, magneticData);

			if (orientation == Configuration.ORIENTATION_PORTRAIT) {
				SensorManager.remapCoordinateSystem(rotationDataTemp, SensorManager.AXIS_Z, SensorManager.AXIS_X, rotationData);
			} else {
				SensorManager.remapCoordinateSystem(rotationDataTemp, SensorManager.AXIS_X, SensorManager.AXIS_MINUS_Z, rotationData);
			}

			tempRotation.set(rotationData[0], rotationData[1], rotationData[2], rotationData[3], rotationData[4], rotationData[5], rotationData[6], rotationData[7], rotationData[8]);

			finalRotation.toIdentity();
			finalRotation.prod(m4);
			finalRotation.prod(m1);
			finalRotation.prod(tempRotation);
			finalRotation.prod(m3);
			finalRotation.prod(m2);
			finalRotation.invert();

			histRotation[histRotationIndex].set(finalRotation);
			histRotationIndex++;
			if (histRotationIndex >= histRotation.length) histRotationIndex = 0;

			smoothRotation.toNull();
			for (int i = 0; i < histRotation.length; i++) {
				smoothRotation.add(histRotation[i]);
			}
			smoothRotation.mult(1 / (float) histRotation.length);

			synchronized (rotationMatrix) {
				rotationMatrix.set(smoothRotation);
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	@Override
	public void onLocationChanged(Location location) {
		try {
			this.location = location;
		} catch (Exception ex) {
			ex.toString();
		}
		refreshItemLocations();
	}

	@Override
	public void onProviderDisabled(String provider) {
	}

	@Override
	public void onProviderEnabled(String provider) {
	}

	@Override
	public void onStatusChanged(String provider, int status, Bundle extras) {
	}

	private void refreshItemLocations() {
		for (AROverlay overlay : overlays) {
			if (overlay instanceof ARItemizedOverlay) {
				((ARItemizedOverlay) overlay).locationUpdated(location);
			}
		}
	}

	private void calcPitchBearing(Matrix3 rotationM) {
		Vector3 looking = new Vector3();
		rotationM.transpose();
		looking.set(1, 0, 0);
		looking.prod(rotationM);
		bearing = (int) (ARUtil.getAngle(0, 0, looking.x, looking.z) + 360) % 360;

		rotationM.transpose();
		looking.set(0, 1, 0);
		looking.prod(rotationM);
		pitch = -ARUtil.getAngle(0, 0, looking.y, looking.z);
	}

	@Override
	public boolean onTouch(View v, MotionEvent event) {
		if (event.getAction() == MotionEvent.ACTION_DOWN) {
			for (int i = 0; i < overlays.size(); i++) {
				if (overlays.get(i) instanceof ARItemizedOverlay) {
					ARItemizedOverlay itemizedOverlay = (ARItemizedOverlay) overlays.get(i);
					if (itemizedOverlay.onTouch(event)) return true;
				}
			}
		}
		return false;
	}

	public void setLocation(Location location) {
		onLocationChanged(location);
	}

}
